Drawing text about it’s visual center

January 29, 2009

Drawing text about it’s visual center is no easy task. Calculating the vertical center of text is tricky because the FontMetrics class doesn’t give you a mechanism to get at the visual bounds – it only gives you the full bounds of a given string, which includes ascent and descent.

When I first set out to do this, I ended up with code that looked like this:

graphics.setFont(new Font("Helvetica", Font.BOLD, 32));

// get the visual center of the component.
int centerX = getWidth()/2;
int centerY = getHeight()/2;

// get the bounds of the string to draw.
FontMetrics fontMetrics = graphics.getFontMetrics();
Rectangle stringBounds = fontMetrics.getStringBounds(text, graphics).getBounds();

// calculate the lower left point at which to draw the string. note that this we
// give the graphics context the y corridinate at which we want the baseline to
// be placed. thus, we calculate our bottom y point by using only the ascent portion
// of the font, so that it is visually centered about the ascent area.
int textX = centerX - stringBounds.width/2;
int textY = centerY + fontMetrics.getAscent()/2;

graphics.drawString(text, textX, textY);

This yielded something like in the following graphic (the red box is the string bounds):

center_text_helvetica1

Everything looks fine, right? OK, lets try changing the font to Lucida Grande and see what happens:

center_text_lucida_grande1

Ugh! What happened? I know nothing about fonts, but Lucida Grande seems to have some extra space in it’s ascent, so centering on the ascent space alone doesn’t yield text that is visually centered.

Back to the drawing board.

All we really need is a mechanism to get the visual bounds of the string we want to draw. After poking around for a while, I found just this in GlyphVector which offers a getVisualBounds method. So lets take another crack at that code:

graphics.setFont(new Font("Helvetica", Font.BOLD, 32));

// get the visual center of the component.
int centerX = getWidth()/2;
int centerY = getHeight()/2;

// get the bounds of the string to draw.
FontMetrics fontMetrics = graphics.getFontMetrics();
Rectangle stringBounds = fontMetrics.getStringBounds(text, graphics).getBounds();

// get the visual bounds of the text using a GlyphVector.
Font font = graphics.getFont();
FontRenderContext renderContext = graphics.getFontRenderContext();
GlyphVector glyphVector = font.createGlyphVector(renderContext, text);
Rectangle visualBounds = glyphVector.getVisualBounds().getBounds();

// calculate the lower left point at which to draw the string. note that this we
// give the graphics context the y corridinate at which we want the baseline to
// be placed. use the visual bounds height to center on in conjuction with the
// position returned in the visual bounds. the vertical position given back in the
// visualBounds is a negative offset from the basline of the text.
int textX = centerX - stringBounds.width/2;
int textY = centerY - visualBounds.height/2 - visualBounds.y;

graphics.drawString(text, textX, textY);

This yields the following when run with Helvetica and Lucida Grande:

center_text_for_real1

Success! The key here is to understand what the y position of the visual bounds represets. The returned bounds is centered about the baseline of the text, which means that the y value is a negative value. This allows us to calculate the basline relative to the visual center of the text, and then pass to the graphics context in order to draw the text.

If there are other ways to do this, I’d love to see them.

11 Responses to “Drawing text about it’s visual center”

  1. Noel Grandin Says:

    Neat!
    I’ve been trying to work out why some of the text my components draw is visually “off”, and this is it.
    Thanks!

  2. jmborer Says:

    Ken, you should be able to get the same result by taking into account the font’s leading.

    Try for example:

    int textY = centerY + fm.getAscent() / 2 – fm.getLeading();

    or

    long y = (getHeight() – Math.round(stringBounds.getHeight())) / 2 + fm.getAscent() – fm.getLeading();

    Extracted from my code, but not tested with yours. The key is the font leading. Let me know if it works.

  3. Ken Says:

    Hi jmbroer,

    I don’t find this technique to work. getLeading returns 0 for both Helvetica and Lucida Grande. Leading refers to the inter-line spacing, so I don’t think it has anything to do with the unused space at the top of the ascent.

    Also, even if this did work you’d still have the trouble of knowing whether a string was taking up the entire given space. A lower case “a” would take up less vertical space then a capital “A”. Thus this vertical height is variable.

    -Ken

  4. jmborer Says:

    You’re right. This method doesn’t take into account the glyph size, but it centers the baseline of the font. It is also correct that this might not work with all fonts.

    Allways interesting to read about this topic. I really appreciate your blog as I also want to create Mac friendly Java applications. :)


  5. […] (still don’t know your last name – sorry Ken!) posts a blog about drawing text around its visual centre. As per usual this post is very helpful, and I recommend anyone doing text layout in Swing (I would […]

  6. John Says:

    I guess an alternative is to use TextLayout:

    TextLayout layout = new TextLayouttext, font, renderContext);
    Rectangle2D visualBounds = layout.getBounds();

    This is the method the stringBounds() javadoc says to use to get a visual bounding box.

  7. Ken Says:

    Sounds like a good solution…which “stringBounds()” method are you referring to?

  8. John Says:

    The getStringBounds() method in Font, which is what the FontMetrics method actually calls. Using TextLayout you don’t need the FontMetrics as the TextLayout gives you the full bounds (as a Rectangle2D), can render thje string using layout.draw(graphics, textX, textY).

  9. sipatha Says:

    thanks for the detailed and useful article. I wanted to ask of the best way to create a financial ticker, currently i fill the area using graphics’ fillRect and re-draw the images and strings. By maintaining counters and positions, its give the moving effect on the screen. Is this the best way to do it? Any help or pointers welcome.

  10. Ray Says:

    Nice. Thank you.

    FYI, I get better results by replacing
    glyphVector.getVisualBounds()
    with
    glyphVector.getLogicalBounds()


Leave a comment